﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using Upreader.Application.Model;

namespace Upreader.Application.Default
{
    /// <summary>
    /// Defines the service for the application
    /// </summary>
    public class ApplicationService : IApplicationService
    {
        #region Fields

        private readonly Func<IDomainContext> _domainContextFactory;

        #endregion // Fields

        #region Constructor

        public ApplicationService(Func<IDomainContext> domainContextFactory)
        {
            if (domainContextFactory == null)
                throw new ArgumentNullException("domainContextFactory");

            _domainContextFactory = domainContextFactory;
        }

        #endregion // Constructor

        #region Download related methods

        public IEnumerable<Download> GetDownloads(Expression<Func<Download, bool>> predicate)
        {
            using (IDomainContext context = CreateDomainContext())
            {
                return context.Downloads.Find(predicate).ToArray();
            }
        }

        public void AddDownload(Download download)
        {
            if (download == null)
                throw new ArgumentNullException("download");

            using (IDomainContext context = CreateDomainContext())
            {
                context.Downloads.Add(download);

                SaveAndRaise(context);
            }
        }

        public void UpdateDownload(Download download)
        {
            if (download == null)
                throw new ArgumentNullException("download");

            using (IDomainContext context = CreateDomainContext())
            {
                context.Downloads.Update(download);

                SaveAndRaise(context);
            }
        }

        public void RemoveDownload(Download download)
        {
            if (download == null)
                throw new ArgumentNullException("download");

            using (IDomainContext context = CreateDomainContext())
            {
                context.Downloads.Remove(download);

                // Remove all files related with this download
                IEnumerable<File> files = context.Files.Find(x => x.DownloadId == download.Id);

                foreach (File file in files)
                {
                    context.Files.Remove(file);
                }

                // Remove all segments. For performance, create a HashSet containing the Id's of all removed files for O(n) lookup
                HashSet<int> removedFileIdSet = new HashSet<int>(files.Select(x => x.Id));
                IEnumerable<Segment> segments = context.Segments.Find(x => removedFileIdSet.Contains(x.FileId));

                foreach (Segment segment in segments)
                {
                    context.Segments.Remove(segment);
                }

                SaveAndRaise(context);
            }
        }


        /// <summary>
        /// Updates the priority of a download by placing it after the download with the ID specified
        /// If beforeDownloadId is null, the download will be prioritized at the end
        /// </summary>
        public void UpdateDownloadPriority(Download download, Download beforeDownload)
        {
            if (download == null)
                throw new ArgumentNullException("download");

            using (IDomainContext context = CreateDomainContext())
            {
                if (beforeDownload == null)
                {
                    int maxPriority = context.Downloads.Max(x => x.Priority);
                    download.Priority = maxPriority + 1;

                    context.Downloads.Update(download);
                }
                else
                {
                    download.Priority = beforeDownload.Priority;
                    IEnumerable<Download> otherDownloads = context.Downloads.Find(x => x.Priority >= download.Priority);
                    foreach (Download otherDownload in otherDownloads)
                    {
                        // for each other download that have the same or a higher priority than the current download, increase priority by one
                        if (otherDownload != download)
                        {
                            otherDownload.Priority++;
                        }
                        context.Downloads.Update(otherDownload);
                    }
                }

                SaveAndRaise(context);
            }
        }

        #endregion // Download related methods

        #region File related methods

        public IEnumerable<File> GetFiles(Expression<Func<File, bool>> predicate)
        {
            using (IDomainContext context = CreateDomainContext())
            {
                return context.Files.Find(predicate).ToArray();
            }
        }

        public void UpdateFile(File file)
        {
            if (file == null)
                throw new ArgumentNullException("file");

            using (IDomainContext context = CreateDomainContext())
            {
                context.Files.Update(file);

                SaveAndRaise(context);
            }
        }

        public void AddFile(File file)
        {
            if (file == null)
                throw new ArgumentNullException("file");

            using (IDomainContext context = CreateDomainContext())
            {
                context.Files.Add(file);

                SaveAndRaise(context);
            }
        }

        public void RemoveFile(File file)
        {
            if (file == null)
                throw new ArgumentNullException("file");

            using (IDomainContext context = CreateDomainContext())
            {
                context.Files.Remove(file);

                // remove all segments associated with this file
                IEnumerable<Segment> segments = context.Segments.Find(x => x.FileId == file.Id);
                foreach (Segment segment in segments)
                {
                    context.Segments.Remove(segment);
                }

                SaveAndRaise(context);
            }
        }

        /// <summary>
        /// Updates the priority of a file by placing it after the file with the ID specified
        /// If beforeFileId is null, the file will be prioritized at the end
        /// </summary>
        public void UpdateFilePriority(File file, File beforeFile)
        {
            if (file == null)
                throw new ArgumentNullException("file");

            using (IDomainContext context = CreateDomainContext())
            {
                if (beforeFile == null)
                {
                    int maxPriority = context.Files.Where(x => x.DownloadId == file.DownloadId)
                                                   .Max(x => x.Priority);
                    file.Priority = maxPriority + 1;
                    context.Files.Update(file);
                }
                else
                {
                    file.Priority = beforeFile.Priority;
                    IEnumerable<File> otherFiles = context.Files.Find(x => x.DownloadId == file.DownloadId && x.Priority >= file.Priority);
                    foreach (File otherFile in otherFiles)
                    {
                        // for each other download that have the same or a higher priority than the current download, increase priority by one
                        if (otherFile != file)
                        {
                            otherFile.Priority++;
                        }
                        context.Files.Update(otherFile);
                    }
                }

                SaveAndRaise(context);
            }
        }

        #endregion // File related methods

        #region Segment related methods

        public IEnumerable<Segment> GetSegments(Expression<Func<Segment, bool>> predicate)
        {
            using (IDomainContext context = CreateDomainContext())
            {
                return context.Segments.Find(predicate).ToArray();
            }
        }

        /// <summary>
        /// Get a set of segments which can be downloaded in priority order
        /// </summary>
        public IEnumerable<Segment> GetSegmentsForDownload(int count, IEnumerable<int> excludeSet)  
        {
            if (excludeSet == null)
                excludeSet = Enumerable.Empty<int>();

            using (IDomainContext context = CreateDomainContext())
            {
                var query = from download in context.Downloads.Find(x => x.State == DownloadState.Unhandeled).OrderBy(x => x.Priority)
                            from file in context.Files.Find(x => x.DownloadId == download.Id && x.State == FileState.Unhandeled).OrderBy(x => x.Priority)
                            from segment in context.Segments.Find(x => x.FileId == file.Id 
                                                                    && x.State == SegmentState.Unhandled 
                                                                    && !excludeSet.Contains(x.Id)).OrderBy(x => x.Index)
                            select segment;

                return query.Take(count).ToArray();
            }
        }

        public void UpdateSegment(Segment segment)
        {
            if (segment == null)
				throw new ArgumentNullException("segment");

            using (IDomainContext context = CreateDomainContext())
            {
                context.Segments.Update(segment);

                SaveAndRaise(context);
            }
        }

        #endregion // Segment related methods

        #region Server related methods

        public IEnumerable<Server> GetServers(Expression<Func<Server, bool>> predicate)
        {
            using (IDomainContext context = CreateDomainContext())
            {
                return context.Servers.Find(predicate).ToArray();
            }
        }

        public void UpdateServer(Server server)
        {
            if (server == null)
                throw new ArgumentNullException("server");

            using (IDomainContext context = CreateDomainContext())
            {
                context.Servers.Update(server);

                SaveAndRaise(context);
            }
        }

        public void AddServer(Server server)
        {
            if (server == null)
                throw new ArgumentNullException("server");

            using (IDomainContext context = CreateDomainContext())
            {
                context.Servers.Add(server);

                SaveAndRaise(context);
            }
        }

        public void RemoveServer(Server server)
        {
            if (server == null)
                throw new ArgumentNullException("server");

            using (IDomainContext context = CreateDomainContext())
            {
                context.Servers.Remove(server);

                SaveAndRaise(context);
            }
        }

        /// <summary>
        /// Updates the priority of a server by placing it after the server with the ID specified
        /// If beforeServerId is null, the file will be prioritized at the end
        /// </summary>
        public void UpdateServerPriority(Server server, Server beforeServer)
        {
            if (server == null)
				throw new ArgumentNullException("server");

            using (IDomainContext context = CreateDomainContext())
            {
                if (beforeServer == null)
                {
                    int maxPriority = context.Downloads.Max(x => x.Priority);
                    server.Priority = maxPriority + 1;

                    context.Servers.Update(server);
                }
                else
                {
                    server.Priority = beforeServer.Priority;
                    IEnumerable<Server> otherServers = context.Servers.Find(x => x.Priority >= server.Priority);
                    foreach (Server otherServer in otherServers)
                    {
                        // for each other download that have the same or a higher priority than the current download, increase priority by one
                        if (otherServer != server)
                        {
                            otherServer.Priority++;
                        }
                        context.Servers.Update(otherServer);
                    }
                }

                SaveAndRaise(context);
            }
        }

        #endregion // Server related methods

        #region Complex methods

        public void ParseNzbStream(System.IO.Stream stream, string fileName)
        {
            using (IDomainContext context = CreateDomainContext())
            {
                NzbParser parser = new NzbParser(context);
                parser.Parse(stream, fileName);

                SaveAndRaise(context);
            }
        }

        #endregion // Complex methods

        #region Protected methods

        protected virtual IDomainContext CreateDomainContext()
        {
            return _domainContextFactory();
        }

        protected virtual void SaveAndRaise(IDomainContext context)
        {
            IModelChanges changes = context.SaveChanges();

            if (changes.ChangedServers != null)
                OnServersChanged(new ModelsChangedEventArgs(changes.ChangedServers));
            if (changes.ChangedDownloads != null)
                OnDownloadsChanged(new ModelsChangedEventArgs(changes.ChangedDownloads));
            if (changes.ChangedFiles!= null)
                OnFilesChanged(new ModelsChangedEventArgs(changes.ChangedFiles));
            if (changes.ChangedSegments != null)
                OnSegmentsChanged(new ModelsChangedEventArgs(changes.ChangedSegments));
        }

        #endregion // Protected methods

        #region EventHandlers

        protected virtual void OnDownloadsChanged(ModelsChangedEventArgs e)
        {
            var eventHandler = DownloadsChanged;

            if (eventHandler != null)
                eventHandler(this, e);
        }

        protected virtual void OnFilesChanged(ModelsChangedEventArgs e)
        {
            var eventHandler = FilesChanged;

            if (eventHandler != null)
                eventHandler(this, e);
        }

        protected virtual void OnSegmentsChanged(ModelsChangedEventArgs e)
        {
            var eventHandler = SegmentsChanged;

            if (eventHandler != null)
                eventHandler(this, e);
        }

        protected virtual void OnServersChanged(ModelsChangedEventArgs e)
        {
            var eventHandler = ServersChanged;

            if (eventHandler != null)
                eventHandler(this, e);
        }

        #endregion // EventHandlers

        #region Events

        /// <summary>
        /// Event raised when one or more downloads are changed
        /// </summary>
        public event ModelsChangedEventHandler DownloadsChanged;

        /// <summary>
        /// Event raised when one or more files are changed
        /// </summary>
        public event ModelsChangedEventHandler FilesChanged;

        /// <summary>
        /// Event raised when one or more segments are changed
        /// </summary>
        public event ModelsChangedEventHandler SegmentsChanged;

        /// <summary>
        /// Event raised when one or more severs are changed
        /// </summary>
        public event ModelsChangedEventHandler ServersChanged;

        #endregion // Events
    }
}
